﻿#region License

// // -------------------------------------------------------------------------------------
// // This information refers to InstrumentationLibrary.cs
// // 
// // Copyright 2012 ELEN7046 Group 2
// // 
// // Last Modified on: 2012-06-10
// // 
// // Contributors:
// // 
// // Amith Sewnarain
// // Elmar Vosloo
// // Jason Thorne
// // Satish Jewan
// // 
// // Wits University 
// // School of Electrical & Information Engineering
// // Software Engineering Tools and Techniques
// // 
// // -------------------------------------------------------------------------------------
// // 
// // This file (InstrumentationLibrary.cs) is part of the DishDash Event Visualization System.
// // 
// // DishDash is free software: you can redistribute it and/or modify
// // it under the terms of the GNU General Public License as published by
// // the Free Software Foundation, either version 3 of the License, or
// // (at your option) any later version.
// // 
// // DishDash is distributed in the hope that it will be useful,
// // but WITHOUT ANY WARRANTY; without even the implied warranty of
// // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// // GNU General Public License for more details.
// // 
// // You should have received a copy of the GNU General Public License
// // along with DishDash.  
// // If not, see <http://www.gnu.org/licenses/>.
// // -------------------------------------------------------------------------------------

#endregion

#region Using Directives

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using InstrumentationLibrary.Config;
using Mono.Cecil;

#endregion

namespace InstrumentationLibrary
{
    public class Instrumentator
    {
        public Instrumentator()
        {
            Assemblies = new Dictionary<string, AssemblyDefinition>();
        }

        private Dictionary<string, AssemblyDefinition> Assemblies { get; set; }

        /// <summary>
        ///   Locate a method for instrumentation. Match on method name and signature.
        /// </summary>
        /// <param name="assembly"> </param>
        /// <param name="typeFullName"> </param>
        /// <param name="methodName"> </param>
        /// <param name="parameterTypes"> </param>
        /// <returns> </returns>
        private MethodDefinition LocateMethod(AssemblyDefinition assembly, string typeFullName, string methodName,
                                              List<string> parameterTypes)
        {
            try
            {
                var fullTypeList = assembly.MainModule.Types;
                var specificType = fullTypeList.First(x => x.FullName.Equals(typeFullName));

                if (specificType == null)
                {
                    throw new Exception(string.Format("Could not find type {0}", typeFullName));
                }

                foreach (var method in specificType.Methods)
                {
                    if (method.Name == methodName & method.Parameters.Count == parameterTypes.Count())
                    {
                        var matched = true;

                        for (var i = 0; i < parameterTypes.Count(); i++)
                        {
                            if (method.Parameters[i].ParameterType.FullName != parameterTypes[i])
                            {
                                matched = false;
                                continue;
                            }
                        }

                        if (matched)
                        {
                            return method;
                        }
                    }
                }
            }
            catch (Exception)
            {
                Console.WriteLine(string.Format("Exception occurred locating {0}, in type {1}, in assembly {2}",
                                                methodName, typeFullName, assembly.Name));
                throw;
            }

            throw new InvalidOperationException(string.Format("method {0} on type {1} was not found", methodName,
                                                              typeFullName));
        }

        public void Instrument(InstrumentationConfig instrumentationConfig)
        {
            instrumentationConfig.Validate();

            AssemblyDefinition assemblyDefinition = null;

            // Get the assembly definition, either from cache or try to load from disk and cache for
            // further modification
            if (Assemblies.ContainsKey(instrumentationConfig.AssemblyFullName))
            {
                assemblyDefinition = Assemblies[instrumentationConfig.AssemblyFullName];
            }
            else
            {
                assemblyDefinition = GetAssemblyDefinition(instrumentationConfig.AssemblyFullName);
                Assemblies.Add(instrumentationConfig.AssemblyFullName, assemblyDefinition);
            }

            // Locate the method to instrument.
            var method =
                LocateMethod(assemblyDefinition, instrumentationConfig.TypeFullName, instrumentationConfig.MethodName,
                             instrumentationConfig.ParameterList);

            // Get the aspect that should be used to instrument the target method.
            var aspect =
                InstrumentationConfiguration.Instance.AspectCatalog[instrumentationConfig.InstrumentationType.ToString()
                    ];

            // Have the aspect instrument this method if it is not already applied
            if (!HasAspect(method, aspect.IdentifyingAttribute.GetType()))
            {
                aspect.Instrument(method);

                // Add custom attribute to identify that the aspect has been applied to this method
                var aspectType = aspect.IdentifyingAttribute.GetType();

                // Get the constructor for the aspect and create a reference in this module to the attribute'a module
                var aspectTypeConstructor = aspectType.GetConstructor(Type.EmptyTypes);
                var constructorReference = method.Module.Import(aspectTypeConstructor);

                // Create a custom attribute with the default constructor
                var instrumentationAttribute = new CustomAttribute(constructorReference);

                // Add the attribute to the custom attribute for the method
                method.CustomAttributes.Add(instrumentationAttribute);
            }
        }

        /// <summary>
        ///   Checks if the identifying attribute of an aspect has been applied to a method.
        /// </summary>
        /// <param name="method"> </param>
        /// <param name="identifyingAttributeType"> </param>
        /// <returns> </returns>
        private bool HasAspect(MethodDefinition method, Type identifyingAttributeType)
        {
            for (int i = 0; i < method.CustomAttributes.Count; i++)
            {
                if (method.CustomAttributes[i].AttributeType.FullName ==
                    identifyingAttributeType.FullName)
                {
                    return true;
                }
            }

            return false;
        }

        private AssemblyDefinition GetAssemblyDefinition(string assemblyFileName)
        {
            var assemblyDefinition =
                AssemblyDefinition.ReadAssembly(Environment.CurrentDirectory + "/" + assemblyFileName);

            return assemblyDefinition;
        }

        /// <summary>
        ///   Saves all changes made and returns a list of paths to instrumented assemblies.
        /// </summary>
        /// <returns> </returns>
        public List<string> SaveChanges()
        {
            var fileNames = new List<string>();

            foreach (var assembly in Assemblies.Values)
            {
                if (!Directory.Exists(@"backup\"))
                {
                    Directory.CreateDirectory(@"backup\");
                }

                var fileName = assembly.Name.Name + ".dll";
                if (File.Exists(fileName))
                {
                    File.Copy(fileName, @"backup\" + fileName + DateTime.Now.ToString(".yyyy.MM.dd.HH.mm.ss"));
                }
                assembly.Write(fileName);
                fileNames.Add(fileName);
            }

            return fileNames;
        }
    }
}